FEAT: Chain-of-Thought Rendering for Attack Results - #2269
Conversation
There was a problem hiding this comment.
Pull request overview
Adds opt-in provider reasoning-summary rendering across conversation, attack-result, and scenario-result output.
Changes:
- Parses and validates OpenAI reasoning-summary payloads.
- Adds pretty and Markdown rendering with reasoning hidden by default.
- Propagates reasoning options through helpers and adds unit coverage.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
pyrit/output/conversation/base.py |
Adds reasoning filtering and parsing. |
pyrit/output/conversation/pretty.py |
Renders pretty reasoning blocks. |
pyrit/output/conversation/markdown.py |
Renders Markdown reasoning blocks. |
pyrit/output/attack_result/base.py |
Extends the rendering contract. |
pyrit/output/attack_result/pretty.py |
Propagates reasoning through pretty output. |
pyrit/output/attack_result/markdown.py |
Propagates reasoning through Markdown output. |
pyrit/output/scenario_result/base.py |
Extends the scenario rendering contract. |
pyrit/output/scenario_result/pretty.py |
Adds scenario-level reasoning output. |
pyrit/output/helpers.py |
Exposes reasoning options in helpers. |
tests/unit/output/conftest.py |
Adds shared reasoning fixtures. |
tests/unit/output/conversation/test_reasoning.py |
Tests parsing, visibility, and formatting. |
tests/unit/output/attack_result/test_pretty.py |
Tests pretty attack reasoning. |
tests/unit/output/attack_result/test_markdown.py |
Tests Markdown attack reasoning. |
tests/unit/output/scenario_result/test_pretty.py |
Tests scenario reasoning output. |
tests/unit/output/test_helpers.py |
Tests helper argument forwarding. |
…c to _render_attack_reasoning_summaries_async for clarity.
…://github.com/ValbuenaVC/PyRIT into vvalbuena-microsoft-plan-cot-output-rendering Merge latest changes from main.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
pyrit/output/scenario_result/pretty.py:341
- When scenario reasoning is enabled, this nested attack printer renders each full objective conversation, and
PrettyConversationMemoryPrinterdisplays everyimage_pathpiece in notebooks. Because this printer is always created with its defaultblur_images=Falseandoutput_scenario_asyncexposes no blur option, requesting reasoning summaries can unexpectedly display unblurred attack images with no way for callers to opt into the safety control available onoutput_attack_async. Please either render only the reasoning blocks or plumb the image-blur settings through the scenario API and nested printer.
attack_result_printer = PrettyAttackResultMemoryPrinter(
sink=sink,
width=width,
indent_size=indent_size,
enable_colors=enable_colors,
)
behnam-o
left a comment
There was a problem hiding this comment.
Looks great! I just have these two comments, one about whether scenario results should even expose this feature and another about how we seem to silently just not print a reasoning section if the model hasn't "reasoned" (I think in that case, it's good to expose/render this fact rather than treating it the same as if the model does not "support/generate" reasoning at all)
| data = json.loads(reasoning_value) | ||
| except (json.JSONDecodeError, TypeError): | ||
| summary = self._extract_reasoning_summary(reasoning_value) | ||
| if not summary: |
There was a problem hiding this comment.
Could we distinguish between an absent reasoning piece and a valid reasoning piece with an empty summary?
When include_reasoning_trace=True:
- If no reasoning piece exists, rendering nothing makes sense because reasoning may be unsupported or may not have been requested.
- If a valid reasoning piece exists but its
summarylist is empty, silently omitting it hides useful state and may leave an empty message section. Could we render something explicit, such as[No reasoning summary was returned by the provider.]? - If the payload is malformed, continuing to raise
ValueErrorseems appropriate.
There was a problem hiding this comment.
I'm not sure I understand your second point. What useful state would be hidden in that context? This is only called on line 114 (for piece in pieces, if this is a reasoning piece, then render the summary). Are you saying we might get a response that's annotated as reasoning (piece.original_value_data_type) and we want the user to be aware of this even when there's no contents in summary?
There was a problem hiding this comment.
yeah exactly. This is how I imagine an end user would use this feature:
I execute an attack and get a response, I'm curious how the model came to that conclusion or what how my prompt was processed in the reasoning phase, so I try to print not only the response, but the reasoning behind it too.
Because this case (explicitly setting include_reasoning) is intentionally done by a user, I think it makes sense to show exactly the reasoning bits, even if it's empty, so they know the model/api itself did not return anything in the reason fields, not that printing/UI is not working okay ...
There was a problem hiding this comment.
Makes perfect sense to compartmentalize concerns in that case. Will update
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…://github.com/ValbuenaVC/PyRIT into vvalbuena-microsoft-plan-cot-output-rendering Resolve merge confict. Adds documentation update to remote.
| # PyRIT adds these visible tags. They label a provider-generated summary | ||
| # and do not represent raw model chain-of-thought. | ||
| block_lines = [ | ||
| r"\<reasoning-summary\>", |
There was a problem hiding this comment.
nit: is there a reason we're using this html type format ? it just doesn't really match the rest of the output imo. like why not just have a section labeled reasoning summary
There was a problem hiding this comment.
Not really. Any objections to something like a bold "💭 Reasoning" heading?
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| @@ -5,7 +5,7 @@ | |||
| # extension: .py | |||
| # format_name: percent | |||
There was a problem hiding this comment.
nit: add something to open ai responses notebook
| Raises: | ||
| ValueError: If the value is not valid JSON or does not match the expected | ||
| OpenAI Responses reasoning-summary shape. | ||
| """ |
There was a problem hiding this comment.
Should we catch this valueerror in pretty.py::_render_reasoning_summary and markdown.py::_format_reasoning_summary ? Since include_reasoning_trace is opt-in and reasoning data is provider-generated (subject to schema drift across OpenAI API versions, or a future non-OpenAI target tagging pieces data_type="reasoning" with a different shape), one malformed reasoning piece will abort rendering of the entire conversation/attack/scenario report rather than just that piece
There was a problem hiding this comment.
Really good catch, I agree
|
plans to add this to the scanner ? |
Co-authored-by: hannahwestra25 <hannahwestra@microsoft.com>
…://github.com/ValbuenaVC/PyRIT into vvalbuena-microsoft-plan-cot-output-rendering Merging in remote changes from PR review.
Not unless you feel it belongs there. Do users want to see the full reasoning trace during a scanner run? I'm not opposed to it, but I assumed the answer was no, though I may be wrong. |



Description
Adds opt-in rendering of OpenAI Responses API reasoning summaries across PyRIT’s output layer.
This PR:
include_reasoning_trace=Falseto conversation and attack-result APIs.No breaking behavior is introduced because reasoning remains opt-in.
Background and Scope
The original story also requested investigation into JSON Schema adoption for scorers and attacks.
That infrastructure and adoption already landed through:
Schemas remain domain-specific; this PR does not introduce a universal response schema or change target/scorer/attack semantics.
CoPyRIT already maps persisted reasoning pieces into its existing Reasoning panel and has mapper/component test coverage. This PR adds the missing PyRIT output parity. CoPyRIT currently has no scenario-results UI, so scenario frontend rendering is outside this PR.
Tests and Documentation
Added or expanded unit coverage for:
Local results:
171 passedpyrit.output:98%statement coverageUpdated the paired output documentation with conversation, attack, and scenario examples and clarification that OpenAI exposes summaries rather than raw chain-of-thought.